#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>

#include "../General/Serializable/serializable.h"
#include "../General/Dictionary/dictionary.h"
#include "../General/ConfigFile/config.h"
#include "../General/Logs/log.h"
#include "Socket-Server.h"
#include "typesRAID.h"
#include "listPPD.h"

uint32_t cantPPD = 0;
uint32_t totalSectores = 0;
int32_t portFS;
int32_t portPPD;
int32_t consoleON; //1 si 0 no
uint32_t maxIdUse = 0;
int32_t logLevel;
t_log * LogFile;
static const char *logFileName = "logFile.txt";

typedef enum {
	FSrequest, PPDrequest
} TypeRequest;

pthread_t hcx,
		  thd_sync[MAX_PPD],
		  thd_recvPPD[MAX_PPD];

PPD PPDController[MAX_PPD];

void _printf(const char * format, ...)
{
	if (!consoleON) return;
	va_list args_list;
	va_start(args_list, format);
	vprintf(format, args_list);
}

pthread_mutex_t mutexIDPPD = PTHREAD_MUTEX_INITIALIZER;

void checkDisks()
{
	//chequea que haya al menos un disco consistente
	int32_t i;
	for(i = 0; i < MAX_PPD; i++) {
		if(PPDController[i].status == PPD_ON && PPDController[i].sincronizedTo >= totalSectores - 1)
			return;
	}
	printf("NINGUN DISCO SE ENCUENTRA SINCRONIZADO, SE FINALIZARA EL SISTEMA\n");
	exit(EXIT_FAILURE);
}

uint32_t FromId = 0; //se usa para distribuir la carga de manera mas equitativa, ya que puede que la lista de un ppd siempre este vacia
uint32_t getBestIdPPD(uint32_t aSector)
{
	uint32_t Result = 0,
			 minor,
			 i;
	pthread_mutex_lock(&mutexIDPPD);
	if (PPDController[FromId].sincronizedTo >= aSector && PPDController[FromId].status == PPD_ON)
		minor = PPDController[FromId].list.count;
	else
		minor = totalSectores + 1;
	i = FromId;
	Result = FromId;
	for (i = 0; i <= maxIdUse; i++) {
		if (PPDController[i].status == PPD_ON   &&
			PPDController[i].list.count < minor &&
			PPDController[i].sincronizedTo >= aSector) {

			minor = PPDController[i].list.count;
			Result = i;
		}
	}
	FromId = (FromId + 1) % (maxIdUse + 1);
	pthread_mutex_unlock(&mutexIDPPD);
	return Result;
}

uint32_t getIdBySocket(int32_t aSocket)
{
	uint32_t Id = 0;
	while(aSocket != PPDController[Id].socket && Id <= maxIdUse)
		Id++;
	if (Id > maxIdUse) {
		Id = -1;
	}
	return Id;
}

uint32_t getFreeIdPPD()
{
	uint32_t i,
			 Result = 0;
	for(i = 0; i < MAX_PPD; i++) {
		if(PPDController[i].status == PPD_OFF && PPDController[i].list.Head == NULL) {
			Result = i;
			break;
		}
	}
	return Result;
}

void InitPPDController()
{
	uint32_t i;
	for(i = 0; i < MAX_PPD; i++) {
		PPDController[i].status = PPD_OFF;
		PPDController[i].list.Head = NULL;
		PPDController[i].sincronizedTo = 0;
	}
}

void ReplanificarPPD(uint32_t ID_PPD)
{
	uint32_t ID_newPPD;
	Nodo_ListPPD * nodoPPD;
	while(PPDController[ID_PPD].list.count > 0) {
		nodoPPD = ListPPD_getAndDisposeFirst(&PPDController[ID_PPD].list);
		ID_newPPD = getBestIdPPD(nodoPPD->data.msg.sectorRequest);
		ListPPD_Push(&PPDController[ID_newPPD].list, nodoPPD->data);
		nodoListPPD_Free(nodoPPD);
	}
	while(PPDController[ID_PPD].listWait.count > 0) {
		nodoPPD = ListPPD_getAndDisposeFirstUnSync(&PPDController[ID_PPD].listWait);
		ID_newPPD = getBestIdPPD(nodoPPD->data.msg.sectorRequest);
		ListPPD_Push(&PPDController[ID_newPPD].list, nodoPPD->data);
		nodoListPPD_Free(nodoPPD);
	}
	nodoPPD = calloc(1, sizeof(Nodo_ListPPD));
	nodoPPD->data.msg.type = msgInternal;
	ListPPD_Push(&PPDController[ID_PPD].list, nodoPPD->data); //envio un mensaje al thread que atiende la cola para que se suicide
}

void thdRecvPedidos(ParamThd * aParam)
{
	Serializable_t aSerializable;
	Nodo_ListPPD * nodoPPD;
	uint32_t Id;
	int32_t statusSocket;
	char verifyConn;

	while(1) {
		nodoPPD = ListPPD_getAndDisposeFirst(&PPDController[aParam->IdPPD].list);
		if(PPDController[aParam->IdPPD].status == PPD_OFF) {
			if (nodoPPD->data.msg.type == msgInternal) { //tengo que salir, el disco se cayo
				free(nodoPPD);
				return;
			}
			if (nodoPPD->data.msg.type != msgSyncRead) {//si era de sync no me importa, pero si era de fs lo tengo que salvar
				Id = getBestIdPPD(nodoPPD->data.msg.sectorRequest);
				ListPPD_Push(&PPDController[Id].list, nodoPPD->data);
				ReplanificarPPD(aParam->IdPPD);
				if (nodoPPD->data.msg.contentSize > 0)
					free(nodoPPD->data.msg.content);
				free(nodoPPD);
				return;//me voy, este disco se desconecto
			}
		}

		Serialize_Message(&aSerializable, nodoPPD->data.msg);

		statusSocket = recv(PPDController[aParam->IdPPD].socket, &verifyConn, 1, MSG_PEEK | MSG_DONTWAIT);
		if (statusSocket != 0 && errno != ECONNREFUSED) {
			if (nodoPPD->data.msg.type == msgSyncRead) {//solicito el pedido al disco que me quede mejor
				Id = getBestIdPPD(nodoPPD->data.msg.sectorRequest);
				printf("Pedido de sincronizacion : %d (a disco : %d)\n", nodoPPD->data.msg.sectorRequest, Id);
				send(PPDController[Id].socket,
					 aSerializable.Data,
					 aSerializable.Size,
					 0);
			}
			else {
				ListPPD_Add(&PPDController[aParam->IdPPD].listWait, nodoPPD->data);
				send(PPDController[aParam->IdPPD].socket/*nodoPPD->data.socket*/, aSerializable.Data, aSerializable.Size, 0);
			}
		}

		statusSocket = recv(PPDController[aParam->IdPPD].socket, &verifyConn, 1, MSG_PEEK | MSG_DONTWAIT);
		if(statusSocket == 0 || errno == ECONNREFUSED) {
			if (nodoPPD->data.msg.type != msgSyncRead) {//si era de sync no me importa, pero si era de fs lo tengo que salvar
				Id = getBestIdPPD(nodoPPD->data.msg.sectorRequest);
				ListPPD_Push(&PPDController[Id].list, nodoPPD->data);
				ReplanificarPPD(aParam->IdPPD);
			}
		}

		if (nodoPPD->data.msg.contentSize > 0)
			free(nodoPPD->data.msg.content);
		free(nodoPPD);
		Serializable_Free(&aSerializable);
		if (statusSocket == 0)
			return;//me voy, este disco se desconecto
	}
}

void SyncDisk(ParamThd * aParam)
{
	//Envia todas las peticiones de lecturas necesarias
	ListPPD_data aDataListPPD;
	uint32_t sector = 0;
	time_t t = time(NULL);
	struct tm tm = *localtime(&t);

	_printf("Inicio sincronizacion de disco Id = %d: %d:%d:%d", aParam->IdPPD, tm.tm_hour, tm.tm_min, tm.tm_sec);
	while(sector < totalSectores && PPDController[aParam->IdPPD].status == PPD_ON) {
		aDataListPPD.msg.sectorRequest = sector;
		aDataListPPD.msg.type = msgSyncRead;
		aDataListPPD.msg.auxiliar = aParam->IdPPD;
		aDataListPPD.msg.contentSize = 0;
		aDataListPPD.msg.content = NULL;
		aDataListPPD.socket = aParam->socket;
		ListPPD_Push(&PPDController[aParam->IdPPD].list, aDataListPPD);
		sector++;
	}
}

int Planificar(int32_t aSocket, TypeRequest typeRequest)
{
	ListPPD_data aDataListPPD;
	Serializable_t aSerializable;
	uint32_t ID_PPD;
	ParamThd aParam;
	char bufferTemp[32];
	time_t t;
	struct tm tm;

	aSerializable.Data = calloc(530, sizeof(char)); //espero recibir un msg desde el FS o un PPD
	aSerializable.Size =
			recv(aSocket, bufferTemp, HeaderSize, MSG_WAITALL);
	memcpy(aSerializable.Data, bufferTemp, HeaderSize);
	if (getSizeFromHeader(bufferTemp) > 0)
		aSerializable.Size +=
				recv(aSocket, aSerializable.Data + HeaderSize, getSizeFromHeader(bufferTemp), MSG_WAITALL);

	Deserialize_Message(&aDataListPPD.msg, aSerializable);

	if (aDataListPPD.msg.type == msgHandShake) {
		if (aDataListPPD.msg.contentSize > 0) {//es handshake de ppd
			totalSectores = aDataListPPD.msg.sectorRequest;
			ID_PPD = getIdBySocket(aSocket);
			_printf("Conexion de planificador de disco: %s (Id = %d)\n", aDataListPPD.msg.content, ID_PPD);
			log_info(LogFile, "", "Conexión de nuevo disco Id = %d", ID_PPD);
			if(ID_PPD >= 0) {
				if(ID_PPD == 0 && cantPPD == 1)
					PPDController[ID_PPD].sincronizedTo = totalSectores - 1;
				else
					PPDController[ID_PPD].sincronizedTo = 0;
				if (cantPPD > 1) {
					aParam.IdPPD  = ID_PPD;
					aParam.socket = aSocket;
					pthread_create(&thd_sync[ID_PPD], NULL, (void *)SyncDisk, (void *)(&aParam));
				}
			}
		}
		send(aSocket, aSerializable.Data, aSerializable.Size, 0);
	}
	else
	if (aDataListPPD.msg.type == msgRead && typeRequest == FSrequest) {
		ID_PPD = getBestIdPPD(aDataListPPD.msg.sectorRequest);
		aDataListPPD.socket = PPDController[ID_PPD].socket;
		aDataListPPD.msg.auxiliar = aSocket; //socket del FS por donde debe volver el msg cuando llegue
		ListPPD_Push(&PPDController[ID_PPD].list, aDataListPPD);
		_printf("Lectura de sector %d a disco : Id = %d\n", aDataListPPD.msg.sectorRequest, ID_PPD);
		log_info(LogFile, "", "Lectura de sector %d a disco : Id = %d", aDataListPPD.msg.sectorRequest, ID_PPD);
	}
	else
	if (aDataListPPD.msg.type == msgRead && typeRequest == PPDrequest) {
		//auxiliar contiene al socket del FS que le corresponde recibir el pedido
		send(aDataListPPD.msg.auxiliar, aSerializable.Data, aSerializable.Size, 0);
		ListPPD_DisposeFirst(&PPDController[getIdBySocket(aSocket)].listWait);
	}
	else
	if (aDataListPPD.msg.type == msgSyncRead) {
		ID_PPD = aDataListPPD.msg.auxiliar; //si es la respuesta de un pedido de sincronizacion se lo paso al ppd que la solicito
		//aDataListPPD.msg.type = msgWrite; //el pedido ahora pasa a ser de escritura a self.ppd
		//PPDController[ID_PPD].socket; //la escritura debe ser en el ppd propio
		//printf("Pedido de sincronizacion : %d\n", aDataListPPD.msg.sectorRequest);
		Serializable_EditType(&aSerializable, msgWrite);//el pedido ahora pasa a ser de escritura a self.ppd
		send(PPDController[ID_PPD].socket,
			 aSerializable.Data,
			 aSerializable.Size,
			 0);
		PPDController[ID_PPD].sincronizedTo = aDataListPPD.msg.sectorRequest;
		if (PPDController[ID_PPD].sincronizedTo == totalSectores - 1) {
			t  = time(NULL);
			tm = *localtime(&t);
			_printf("Fin sincronizacion de disco Id = %d: %d:%d:%d\n", ID_PPD, tm.tm_hour, tm.tm_min, tm.tm_sec);
			log_info(LogFile, "", "Nuevo disco sincronizado (Id = %d)", ID_PPD);
		}
	}
	else
	if (aDataListPPD.msg.type == msgWrite && typeRequest == FSrequest) { //si es del ppd por ahora nada...
		send(aSocket, aSerializable.Data, HeaderSize, 0);
		_printf("Escritura en todos los discos\n");
		for (ID_PPD = 0; ID_PPD < cantPPD; ID_PPD++) {
			aDataListPPD.socket = PPDController[ID_PPD].socket;
			ListPPD_Push(&PPDController[ID_PPD].list, aDataListPPD);
		}
	}

	Message_Free(&aDataListPPD.msg);
	Serializable_Free(&aSerializable);
	return 0;
}

void loadConfigInformation()
{
/*
	puertoPPD XXXX
	puertoFS XXXX
	consola 0/1 (1 activada, 0 desactivada)
*/
	const char * configFileName = "configRAID.txt";
	char valor[30];

	Dictionary parametros;
	Dictionary_Create(&parametros);
	Dictionary_Add(&parametros, "puertoPPD", "");
	Dictionary_Add(&parametros, "puertoFS", "");
	Dictionary_Add(&parametros, "consola", "");
	Dictionary_Add(&parametros, "LogLevel", "");

	if (CargarParametros(configFileName, &parametros) == -1) {
		perror("No se pudieron cargar los parametros de configuracion");
		exit(EXIT_FAILURE); //No se debe seguir si no se puede levantar el configFile
	}

	Dictionary_Index(&parametros, "puertoPPD", valor);
	portPPD = atoi(valor);
	Dictionary_Index(&parametros, "puertoFS", valor);
	portFS = atoi(valor);
	Dictionary_Index(&parametros, "consola", valor);
	consoleON = atoi(valor);
	Dictionary_Index(&parametros, "LogLevel", valor);
	logLevel = atoi(valor);

	Dictionary_Destroy(&parametros);
}

int main(void){

	char bufferVerifyPPD[8]; //se usa para verificar si no se cayo el ppd

	loadConfigInformation();
    LogFile = log_create("ProcesoRAID", logFileName, logLevel, M_CONSOLE_DISABLE);
	//socketEntradaFS escucha los pedidos del FS,
	//socketEntradaPPD escucha las conexiones del PPD (los ppd que se le conectan)
	int32_t socketEntradaFS = CreaConexionServer_INET(portFS);
	int32_t socketEntradaPPD = CreaConexionServer_INET(portPPD);

	struct sockaddr_in direccionRem;
	socklen_t direccionRemSize;
	int32_t sock1;
	fd_set fds;
	int32_t maxfd, i,minfd;
	uint32_t id = 0;
	ParamThd aParam[MAX_PPD];

	InitPPDController();

	FD_ZERO(&fds);
	FD_SET(socketEntradaFS, &fds);
	FD_SET(socketEntradaPPD, &fds);
	maxfd = socketEntradaFS;
	minfd = socketEntradaPPD;
	if (socketEntradaPPD > maxfd) {
		maxfd = socketEntradaPPD;
		minfd = socketEntradaFS;
	}

	list_sockets aSocketListFS, aSocketListPPD;
	nodo_socket * auxfd;
	aSocketListFS.list = NULL;
	aSocketListPPD.list = NULL;

	_printf("PROCESO RAID EN ESPERA\n");
	while (1) {
		if (select(maxfd + 1, &fds, NULL, NULL, NULL) != -1) {
			for (i = minfd; i <= maxfd; i++) {
				if (FD_ISSET(i, &fds)) { //Ocurrio un evento en el fd i
					if (list_sockets_exists(&aSocketListFS, i) != -1) { //Es un socket activo del FS
						Planificar(i, FSrequest);
					}
					else
					if (list_sockets_exists(&aSocketListPPD, i) != -1) { //Es un socket activo del PPD
						if (recv(i, bufferVerifyPPD, 1, MSG_PEEK) == 0) { //verifico si se cayo el disco
							log_info(LogFile, "", "Desconexión de disco: Id = %d", getIdBySocket(i));
							PPDController[getIdBySocket(i)].status = PPD_OFF; //lo apago
							ReplanificarPPD(getIdBySocket(i));
							list_sockets_delete(&aSocketListPPD, i);
							cantPPD--;
							close(i);
							checkDisks();
						}
						else
							Planificar(i, PPDrequest);
					}
					else
					if (i == socketEntradaFS) {
						sock1 = accept(socketEntradaFS, (struct sockaddr*) &direccionRem, &direccionRemSize);
						if (cantPPD == 0) {
							close(sock1); //si no hay discos no acepto la conexion del FS
							break;
						}
						list_sockets_add(&aSocketListFS, sock1);
						FD_SET(sock1, &fds); //mete el descriptor sock1 al puntero fds
						if(sock1 > maxfd)
							maxfd = sock1;
						i = sock1;
					}
					else
					if (i == socketEntradaPPD) {
						sock1 = accept(socketEntradaPPD, (struct sockaddr*) &direccionRem, &direccionRemSize);
						id = getFreeIdPPD();
						PPDController[id].socket = sock1;
						PPDController[id].status = PPD_ON;
						ListPPD_Create(&PPDController[id].list);
						aParam[id].IdPPD = id;
						list_sockets_add(&aSocketListPPD, sock1);
						FD_SET(sock1, &fds);
						if (sock1 > maxfd)
							maxfd = sock1;
						aParam[id].socket = sock1;
						pthread_create(&thd_recvPPD[id], NULL, (void *)thdRecvPedidos, (void *)(&aParam[id]));
						if(id > maxIdUse) maxIdUse = id;
						//id++;
						cantPPD++;
					}
				}
			} //end for()
			FD_ZERO(&fds);
			FD_SET(socketEntradaFS, &fds);
			FD_SET(socketEntradaPPD, &fds);
			auxfd = aSocketListFS.list;
			while(auxfd != NULL) {
				FD_SET(auxfd->descriptor, &fds);
				auxfd = auxfd->nxt;
			}
			auxfd = aSocketListPPD.list;
			while(auxfd != NULL) {
				FD_SET(auxfd->descriptor, &fds);
				auxfd = auxfd->nxt;
			}
		}
	}

	close(socketEntradaFS);
	close(socketEntradaPPD);

	return EXIT_SUCCESS;
}

